﻿/**
 * Copyright (c) 2008 Justin Etheredge http://www.codethinked.com
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Microsoft Public License (Ms-PL)
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/ms-pl.html
 * If redistributing this code, this entire header must remain intact.
 * 
 * Some of the code in this application has been taken 
 * from the following sources (all of which are licensed under
 * the Ms-PL):
 * Matt Warren's Blog: http://blogs.msdn.com/mattwar/
 * MSDN: http://msdn2.microsoft.com/en-us/library/bb546158.aspx
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using LinqToSimpleDB.HelperClasses;
using LinqToSimpleDB.Exceptions;
using System.Globalization;

namespace LinqToSimpleDB
{
  internal class QueryTranslator : HelperClasses.ExpressionVisitor
  {
    private Expression expression = null;
    private StringBuilder query = null;
    private string lastAttribute = string.Empty;

    private int precision = 0;
    private int scale = 0;

    private int whereDepth = 0;

    private bool inBrackets = false;

    public QueryTranslator(Expression exp, int precision, int scale)
    {
      this.expression = exp;
      this.precision = precision;
      this.scale = scale;
    }

    public string Query
    {
      get
      {
        if (query == null)
        {
          query = new StringBuilder();
          //query.Append("[");
          this.Visit(this.expression);
          if (inBrackets)
          {
            query.Append("]");
            inBrackets = false;
          }          
        }
        string queryText = query.ToString();

        return queryText.Length <= 2 ? string.Empty : queryText;
      }
    }

    protected override Expression VisitUnary(UnaryExpression ue)
    {
      switch(ue.NodeType)
      {
        case ExpressionType.Not:
          query.Append("not ");
          break;
        case ExpressionType.Quote:
          return ue;
        default:
          throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "The unary operator '{0}' is not supported", ue.NodeType));
      }
      this.Visit(ue.Operand);
      return ue;
    }

    protected override Expression VisitBinary(BinaryExpression be)
    {
      if (!inBrackets)
      {
        query.Append("[");
        inBrackets = true;
      }      
      this.Visit(be.Left);
      switch (be.NodeType)
      {
        case ExpressionType.And:
        case ExpressionType.AndAlso:
          query.Append(" and ");
          break;
        case ExpressionType.Or:
        case ExpressionType.OrElse:
          query.Append(" or ");
          break;
        case ExpressionType.Equal:
          query.Append(" = ");
          break;
        case ExpressionType.NotEqual:
          query.Append(" != ");
          break;
        case ExpressionType.LessThan:
          query.Append(" < ");
          break;
        case ExpressionType.LessThanOrEqual:
          query.Append(" <= ");
          break;
        case ExpressionType.GreaterThan:
          query.Append(" > ");
          break;
        case ExpressionType.GreaterThanOrEqual:
          query.Append(" >= ");
          break;
        default:
          throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "The binary operator '{0}' is not supported", be.NodeType));
      }
      this.Visit(be.Right);
      return be;
    }

    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
      if (m.Object == null && m.Method.Name == "Where")
      {
        LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(m.Arguments[1])).Operand;

        // Send the lambda expression through the partial evaluator.
        lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);

        whereDepth++;
        if (whereDepth > 1)
        {
          throw new InvalidQueryException("SimpleDB does not support nested \"WHERE\" clauses");
        }
        Visit(m.Arguments[0]);
        Visit(lambdaExpression.Body);

        whereDepth--;

        return m;
      }
      else if (m.Object != null && m.Object.Type == typeof(SimpleDBAttributeCollection) && String.Equals(m.Method.Name, "get_Item"))
      {
        string val = m.Arguments[0].ToString();
        if (val.Length > 2)
        {
          string attribute = val.Substring(1, val.Length - 2);
          if (!String.IsNullOrEmpty(lastAttribute) && attribute != lastAttribute)
          {
            throw new Exceptions.InvalidAttributeException("You cannot use more than one apiAttribute in a single query, use a Union or Intersect to join multiple attributes");
          }
          lastAttribute = attribute;
          query.Append("'");
          query.Append(attribute);
          query.Append("'");
        }
        return m;
      }
      else if (m.Object == null && String.Equals(m.Method.Name, "Union"))
      {
        //reset the last apiAttribute        
        this.Visit(m.Arguments[0]);
        if (inBrackets)
        {
          query.Append("]");
          inBrackets = false;
        }
        query.Append(" UNION ");
        lastAttribute = string.Empty;
        this.Visit(m.Arguments[1]);
        return m;
      }
      else if (m.Object == null && String.Equals(m.Method.Name, "Intersect"))
      {
        //reset the last apiAttribute
        this.Visit(m.Arguments[0]);
        if (inBrackets)
        {
          query.Append("]");
          inBrackets = false;
        }
        query.Append(" INTERSECT ");
        lastAttribute = string.Empty;
        this.Visit(m.Arguments[1]);
        return m;
      }
      else if (m.Object == null && String.Equals(m.Method.Name, "Join"))
      {
        throw new InvalidQueryException("SimpleDB does not support joins.");
      }
      else if (m.Object == null && String.Equals(m.Method.Name, "OrderBy"))
      {
        throw new InvalidQueryException("SimpleDB does not support ordering.");
      }
      else if (m.Method.DeclaringType == typeof(SimpleDBAttribute) && m.Method.Name == "StartsWith")
      {
        if (!inBrackets)
        {
          query.Append("[");
          inBrackets = true;
        }
        this.Visit(m.Object);
        query.Append(" starts-with ");
        this.Visit(m.Arguments[0]);
        return m;
      }
      return base.VisitMethodCall(m);
    }

    protected override Expression VisitConstant(ConstantExpression c)
    {
      IQueryable q = c.Value as IQueryable;
      if (q == null)
      {
        switch (Type.GetTypeCode(c.Value.GetType()))
        {
          case TypeCode.Boolean:
            query.Append("'");
            query.Append(((bool)c.Value) ? "true" : "false");
            query.Append("'");
            break;
          case TypeCode.String:
            query.Append("'");
            query.Append(c.Value);
            query.Append("'");
            break;
          case TypeCode.Decimal:
            query.Append("'");
            query.Append(SimpleDBNumericHelper.PadAndFormatNumber((Decimal)c.Value, precision, scale));
            query.Append("'");
            break;
          case TypeCode.Single:
          case TypeCode.Double:
            query.Append("'");
            query.Append(SimpleDBNumericHelper.PadAndFormatNumber((Double)c.Value, precision, scale));
            query.Append("'");
            break;
          case TypeCode.SByte:
          case TypeCode.Int16:
          case TypeCode.Int32:
            query.Append("'");
            query.Append(SimpleDBNumericHelper.PadAndFormatNumber((Int32)c.Value, precision, scale));
            query.Append("'");
            break;
          case TypeCode.Int64:
            query.Append("'");
            query.Append(SimpleDBNumericHelper.PadAndFormatNumber((Int64)c.Value, precision, scale));
            query.Append("'");
            break;
          case TypeCode.Byte:
          case TypeCode.UInt16:
          case TypeCode.UInt32:
            query.Append("'");
            query.Append(SimpleDBNumericHelper.PadAndFormatNumber((UInt32)c.Value, precision, scale));
            query.Append("'");
            break;
          case TypeCode.UInt64:
            query.Append("'");
            query.Append(SimpleDBNumericHelper.PadAndFormatNumber((UInt64)c.Value, precision, scale));
            query.Append("'");
            break;
          case TypeCode.Object:
            throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "The constant for '{0}' is not supported", c.Value));
          default:
            throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "The constant for '{0}' is not supported", c.Value));
        }
      }
      return c;
    }
  }
}
